package antonst.map;

import java.util.Iterator;
import java.util.NoSuchElementException;

public class MyHashMap<K, V> implements MyMap<K, V> {

	private static final int DEFAULT_CAPACITY = 16;
	private static final float DEFAULT_LOAD_FACTOR = 0.75f;
	private static final int BUCKET_NUMBER_FOR_NULL_KEY = 0;

	private SimpleEntry<K, V>[] entryArray;
	private int size = 0;
	private int capacity;
	private final float loadFactor;

	public MyHashMap() {
		this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
	}

	public MyHashMap(int initialCapacity) {
		this(initialCapacity, DEFAULT_LOAD_FACTOR);
	}

	public MyHashMap(int initialCapacity, float loadFactor) {
		String messageCapacity = "Initial capacity cannot be negative!";
		if (initialCapacity < 0) {
			throw new IllegalArgumentException(messageCapacity);
		}
		String messageLoadFactor = "Load factor has to be greater than 0!";
		if (loadFactor <= 0) {
			throw new IllegalArgumentException(messageLoadFactor);
		}
		capacity = initialCapacity;
		this.loadFactor = loadFactor;
		int arrayLength = calculateRequireArrayLength(capacity, loadFactor);
		entryArray = (SimpleEntry<K, V>[]) new SimpleEntry[arrayLength];
	}

	@Override
	public void clear() {
		size = 0;
		for (int i = 0; i < entryArray.length; i++) {
			entryArray[i] = null;
		}
	}

	@Override
	public boolean containsKey(Object key) {
		for (SimpleEntry entry : entryArray) {
			SimpleEntry next = entry;
			while (next != null) {
				if (next.getKey().equals(key)) {
					return true;
				}
				next = next.getNext();
			}
		}
		return false;
	}

	@Override
	public boolean containsValue(Object value) {
		for (SimpleEntry entry : entryArray) {
			SimpleEntry next = entry;
			while (next != null) {
				if (next.getValue().equals(value)) {
					return true;
				}
				next = next.getNext();
			}
		}
		return false;
	}

	@Override
	public V get(Object key) {
		int bucketIndex = getBucketIndexForKey(entryArray, key);
		SimpleEntry<K, V> next = entryArray[bucketIndex];
		while (next != null) {
			if (key == null && next.getKey() == null) {
				return next.getValue();
			}
			if (next.getKey().equals(key)) {
				return next.getValue();
			}
			next = next.getNext();
		}
		return null;
	}

	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	@Override
	public V put(K key, V value) {
		int bucketIndex = getBucketIndexForKey(entryArray, key);
		SimpleEntry<K, V> next = entryArray[bucketIndex];
		boolean alreadyInList = false;
		while (next != null && !alreadyInList) {
			if (next.getKey().equals(key)) {
				alreadyInList = true;
			} else {
				next = next.getNext();
			}
		}
		if (alreadyInList) {
			next.setValue(value);
		} else {
			checkEnoughCapacity();
			SimpleEntry<K, V> entryToPut = new SimpleEntry<>(key, value, entryArray[bucketIndex]);
			entryArray[bucketIndex] = entryToPut;
			size = size + 1;
		}
		return value;
	}

	@Override
	public V remove(Object key) {
		int bucketIndex = getBucketIndexForKey(entryArray, key);
		SimpleEntry<K, V> baseEntry = entryArray[bucketIndex];
		if (baseEntry.getKey().equals(key)) {
			entryArray[bucketIndex] = baseEntry.getNext();
			size = size - 1;
			return baseEntry.getValue();
		}
		SimpleEntry<K, V> previous = baseEntry;
		SimpleEntry<K, V> next = baseEntry.getNext();
		while (next != null) {
			if (next.getKey().equals(key)) {
				previous.setNewNext(next.getNext());
				size = size - 1;
				return next.getValue();
			}
			previous = next;
			next = next.getNext();
		}
		return null;
	}

	@Override
	public int size() {
		return size;
	}

	@Override
	public Iterator<MyMap.Entry<K, V>> entryIterator() {
		return new EntryIterator();
	}

	private int calculateRequireArrayLength(int requireCapacity,
			float requireLoadFactor) {
		int arrayLength = (int) Math.ceil(requireCapacity * requireLoadFactor);
		return arrayLength;
	}

	private SimpleEntry<K, V> getFirstEntryInBucket(int bucketNumber) {
		return entryArray[bucketNumber];
	}

	private void checkEnoughCapacity() {
		if (size + 1 > capacity) {
			int arrayLength = calculateRequireArrayLength(capacity * 2, loadFactor);
			entryArray = rehash(entryArray, arrayLength);
		}
	}

	private int getBucketIndexForKey(SimpleEntry<?, ?>[] array, Object key) {
		if (key == null) {
			return BUCKET_NUMBER_FOR_NULL_KEY;
		} else {
			return Math.abs(key.hashCode()) % (array.length - 1);
		}
	}

	private SimpleEntry<K, V>[] rehash(SimpleEntry<K, V>[] arrayToRehash, int lengthOfNewArray) {
		SimpleEntry<K, V>[] newArray = (SimpleEntry<K, V>[]) new SimpleEntry[lengthOfNewArray];
		for (SimpleEntry<K, V> baseEntry : arrayToRehash) {
			SimpleEntry<K, V> next = baseEntry;
			while (next != null) {
				putIntoHashArray(newArray, next.getKey(), next.getValue());
			}
		}
		return newArray;
	}

	private void putIntoHashArray(SimpleEntry<K, V>[] newArray, K key, V value) {
		int bucketIndex = getBucketIndexForKey(newArray, key);
		SimpleEntry<K, V> next = newArray[bucketIndex];
		SimpleEntry<K, V> entryToPut = new SimpleEntry<>(key, value, newArray[bucketIndex]);
		newArray[bucketIndex] = entryToPut;
	}

	public static class SimpleEntry<K, V> implements MyMap.Entry<K, V> {

		private final K key;
		private V value;
		private SimpleEntry<K, V> next;

		public SimpleEntry(K key, V value, SimpleEntry<K, V> next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}

		@Override
		public K getKey() {
			return key;
		}

		@Override
		public V getValue() {
			return value;
		}

		@Override
		public V setValue(V value) {
			this.value = value;
			return value;
		}

		void setNewNext(SimpleEntry<K, V> newNext) {
			next = newNext;
		}

		SimpleEntry<K, V> getNext() {
			return next;
		}
		
		@Override
		public boolean equals(Object obj) {
			if (obj == null) return false;
			if (obj instanceof SimpleEntry){
				SimpleEntry other = (SimpleEntry) obj; 
				if (key == null && other.key == null) {
					return value.equals(other.value);
				} else if (value == null && other.value == null) {
					return key.equals(other.key);
				} else {
					return key.equals(other.key) & value.equals(other.value);
				}
			}
			return false;
		} 

		@Override
		public int hashCode() {
			int keyHashCode = (key == null) ? 0 : key.hashCode();
			int valueHashCode = (value == null) ? 0 : value.hashCode();
			return keyHashCode ^ valueHashCode;
		}

	}

	private class EntryIterator implements Iterator<Entry<K, V>> {

		private int elementsReturned = 0;
		private int bucketIndex = 0;
		private SimpleEntry<K, V> lastReturned = null;

		@Override
		public boolean hasNext() {
			return (elementsReturned < MyHashMap.this.size);
		}

		@Override
		public Entry<K, V> next() {
			if (lastReturned == null) {
				getNextNotNullArrayEntry();
				return lastReturned;
			} else {
				if (lastReturned.getNext() == null) {
					getNextNotNullArrayEntry();
					return lastReturned;
				} else {
					lastReturned = lastReturned.getNext();
					elementsReturned = elementsReturned + 1;
					return lastReturned;
				}
			}
		}

		private void getNextNotNullArrayEntry() {
			while (bucketIndex < MyHashMap.this.entryArray.length) {
				if (MyHashMap.this.entryArray[bucketIndex] != null) {
					lastReturned = MyHashMap.this.entryArray[bucketIndex];
					elementsReturned = elementsReturned + 1;
					bucketIndex++;
					return;
				}
			}
			throw new NoSuchElementException();
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
		}

	}
}
